Проект: Анализ резюме из HeadHunter
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
import plotly.express as px
Исследование структуры данных¶
- Прочитайте данные с помощью библиотеки Pandas. Совет: перед чтением обратите внимание на разделитель внутри файла.
#ваш код здесь
hh = pd.read_csv("D:/IDE/SF - Assign/hh.csv", sep= ";")
print(hh.shape)
(44744, 12)
- Выведите несколько первых (последних) строк таблицы, чтобы убедиться в том, что ваши данные не повреждены. Ознакомьтесь с признаками и их структурой.
#ваш код здесь
display(hh.head(10))
| Пол, возраст | ЗП | Ищет работу на должность: | Город, переезд, командировки | Занятость | График | Опыт работы | Последнее/нынешнее место работы | Последняя/нынешняя должность | Образование и ВУЗ | Обновление резюме | Авто | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | Мужчина , 39 лет , родился 27 ноября 1979 | 29000 руб. | Системный администратор | Советск (Калининградская область) , не готов к... | частичная занятость, проектная работа, полная ... | гибкий график, полный день, сменный график, ва... | Опыт работы 16 лет 10 месяцев Август 2010 — п... | МАОУ "СОШ № 1 г.Немана" | Системный администратор | Неоконченное высшее образование 2000 Балтийск... | 16.04.2019 15:59 | Имеется собственный автомобиль |
| 1 | Мужчина , 60 лет , родился 20 марта 1959 | 40000 руб. | Технический писатель | Королев , не готов к переезду , готов к редким... | частичная занятость, проектная работа, полная ... | гибкий график, полный день, сменный график, уд... | Опыт работы 19 лет 5 месяцев Январь 2000 — по... | Временный трудовой коллектив | Менеджер проекта, Аналитик, Технический писатель | Высшее образование 1981 Военно-космическая ак... | 12.04.2019 08:42 | Не указано |
| 2 | Женщина , 36 лет , родилась 12 августа 1982 | 20000 руб. | Оператор | Тверь , не готова к переезду , не готова к ком... | полная занятость | полный день | Опыт работы 10 лет 3 месяца Октябрь 2004 — Де... | ПАО Сбербанк | Кассир-операционист | Среднее специальное образование 2002 Профессио... | 16.04.2019 08:35 | Не указано |
| 3 | Мужчина , 38 лет , родился 25 июня 1980 | 100000 руб. | Веб-разработчик (HTML / CSS / JS / PHP / базы ... | Саратов , не готов к переезду , готов к редким... | частичная занятость, проектная работа, полная ... | гибкий график, удаленная работа | Опыт работы 18 лет 9 месяцев Август 2017 — Ап... | OpenSoft | Инженер-программист | Высшее образование 2002 Саратовский государст... | 08.04.2019 14:23 | Не указано |
| 4 | Женщина , 26 лет , родилась 3 марта 1993 | 140000 руб. | Региональный менеджер по продажам | Москва , не готова к переезду , готова к коман... | полная занятость | полный день | Опыт работы 5 лет 7 месяцев Региональный мене... | Мармелад | Менеджер по продажам | Высшее образование 2015 Кгу Психологии и педаг... | 22.04.2019 10:32 | Не указано |
| 5 | Мужчина , 29 лет , родился 5 октября 1989 | 25000 руб. | Технический специалист | Старый Оскол , не готов к переезду , не готов ... | полная занятость | полный день, сменный график | Опыт работы 9 лет 9 месяцев Технический специ... | Комбинат хлебопродуктов Старооскольский | Слесарь КИПиА | Неоконченное высшее образование 2013 Воронежс... | 22.04.2019 15:59 | Имеется собственный автомобиль |
| 6 | Мужчина , 46 лет , родился 19 сентября 1972 | 250000 руб. | Руководитель ИТ-проектов | Москва , не готов к переезду , готов к редким ... | полная занятость | полный день | Опыт работы 22 года 9 месяцев Руководитель ИТ... | СИБИНТЕК, ООО ИК | Менеджер (Руководитель ИТ-проектов) | Высшее образование 2008 ФГОУ ВПО «Уральская ак... | 25.04.2019 22:48 | Не указано |
| 7 | Мужчина , 29 лет , родился 9 июля 1989 | 70000 руб. | Инженер АСУ ТП | Москва , м. Бульвар Рокоссовского , готов к п... | полная занятость | полный день | Опыт работы 3 года 11 месяцев Декабрь 2016 — ... | ФМ-инжиниринг | Инженер АСУ ТП | Высшее образование 2014 Белорусская Государств... | 07.05.2019 17:59 | Не указано |
| 8 | Мужчина , 29 лет , родился 11 июля 1989 | 65000 руб. | Ревизор | Москва , м. Шоссе Энтузиастов , готов к перее... | полная занятость | полный день | Опыт работы 8 лет 9 месяцев Декабрь 2016 — по... | ФГБУ РСВО | Старший специалист | Неоконченное высшее образование 2020 Московск... | 11.04.2019 11:08 | Имеется собственный автомобиль |
| 9 | Мужчина , 34 года , родился 26 мая 1984 | 55000 руб. | Менеджер по работе с клиентами, Pre-sale менед... | Москва , м. Теплый Стан , не готов к переезду... | полная занятость | полный день | Опыт работы 16 лет 6 месяцев Июнь 2018 — по н... | ООО "Мираском" | Специалист отдела ИТ | Высшее образование 2007 Московский государств... | 19.04.2019 11:39 | Имеется собственный автомобиль |
- Выведите основную информацию о числе непустых значений в столбцах и их типах в таблице.
- Обратите внимание на информацию о числе непустых значений.
#ваш код здесь
display(hh.info())
<class 'pandas.core.frame.DataFrame'> RangeIndex: 44744 entries, 0 to 44743 Data columns (total 12 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 Пол, возраст 44744 non-null object 1 ЗП 44744 non-null object 2 Ищет работу на должность: 44744 non-null object 3 Город, переезд, командировки 44744 non-null object 4 Занятость 44744 non-null object 5 График 44744 non-null object 6 Опыт работы 44576 non-null object 7 Последнее/нынешнее место работы 44743 non-null object 8 Последняя/нынешняя должность 44742 non-null object 9 Образование и ВУЗ 44744 non-null object 10 Обновление резюме 44744 non-null object 11 Авто 44744 non-null object dtypes: object(12) memory usage: 4.1+ MB
None
- Выведите основную статистическую информацию о столбцах.
#ваш код здесь
display(hh.describe())
| Пол, возраст | ЗП | Ищет работу на должность: | Город, переезд, командировки | Занятость | График | Опыт работы | Последнее/нынешнее место работы | Последняя/нынешняя должность | Образование и ВУЗ | Обновление резюме | Авто | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| count | 44744 | 44744 | 44744 | 44744 | 44744 | 44744 | 44576 | 44743 | 44742 | 44744 | 44744 | 44744 |
| unique | 16003 | 690 | 14929 | 10063 | 38 | 47 | 44413 | 30214 | 16927 | 40148 | 18838 | 2 |
| top | Мужчина , 32 года , родился 17 сентября 1986 | 50000 руб. | Системный администратор | Москва , не готов к переезду , не готов к кома... | полная занятость | полный день | Опыт работы 10 лет 8 месяцев Апрель 2018 — по... | Индивидуальное предпринимательство / частная п... | Системный администратор | Высшее образование 1987 Военный инженерный Кра... | 07.05.2019 09:50 | Не указано |
| freq | 18 | 4064 | 3099 | 1261 | 30026 | 22727 | 3 | 935 | 2062 | 4 | 25 | 32268 |
Преобразование данных¶
- Начнем с простого - с признака "Образование и ВУЗ". Его текущий формат это: <Уровень образования год выпуска ВУЗ специальность...>. Например:
- Высшее образование 2016 Московский авиационный институт (национальный исследовательский университет)...
- Неоконченное высшее образование 2000 Балтийская государственная академия рыбопромыслового флота…
Нас будет интересовать только уровень образования.
Создайте с помощью функции-преобразования новый признак "Образование", который должен иметь 4 категории: "высшее", "неоконченное высшее", "среднее специальное" и "среднее".
Выполните преобразование, ответьте на контрольные вопросы и удалите признак "Образование и ВУЗ".
Совет: обратите внимание на структуру текста в столбце "Образование и ВУЗ". Гарантируется, что текущий уровень образования соискателя всегда находится в первых 2ух слов и начинается с заглавной буквы. Воспользуйтесь этим.
Совет: проверяйте полученные категории, например, с помощью метода unique()
#ваш код здесь
def edu_lvl(edu_str):
if 'высшее' in edu_str.lower():
if 'неоконченное' in edu_str.lower():
return 'неоконченное высшее'
else:
return 'высшее'
elif 'среднее специальное' in edu_str.lower():
return 'среднее специальное'
elif 'среднее образование' in edu_str.lower():
return 'среднее'
# Применяем написанную функцию на создаваемый столбец
hh['Образование'] = hh['Образование и ВУЗ'].apply(lambda x: edu_lvl(x))
hh.drop('Образование и ВУЗ', axis = 1, inplace = True)
print(hh["Образование"].unique())
['неоконченное высшее' 'высшее' 'среднее специальное' 'среднее']
- Теперь нас интересует столбец "Пол, возраст". Сейчас он представлен в формате <Пол , возраст , дата рождения >. Например:
- Мужчина , 39 лет , родился 27 ноября 1979
- Женщина , 21 год , родилась 13 января 2000
Как вы понимаете, нам необходимо выделить каждый параметр в отдельный столбец.
Создайте два новых признака "Пол" и "Возраст". При этом важно учесть:
- Признак пола должен иметь 2 уникальных строковых значения: 'М' - мужчина, 'Ж' - женщина.
- Признак возраста должен быть представлен целыми числами.
Выполните преобразование, ответьте на контрольные вопросы и удалите признак "Пол, возраст" из таблицы.
Совет: обратите внимание на структуру текста в столбце, в части на то, как разделены параметры пола, возраста и даты рождения между собой - символом ' , '. Гарантируется, что структура одинакова для всех строк в таблице. Вы можете воспользоваться этим.
#ваш код здесь
def gen_age(info):
parts = info.split(' , ')
gender = 'М' if 'Мужчина' in parts[0] else 'Ж'
age = int(parts[1].split()[0])
return pd.Series([gender, age])
# Применяем функцию на создаваемые столбцы
hh[['Пол', 'Возраст']] = hh['Пол, возраст'].apply(gen_age)
hh.drop(columns=['Пол, возраст'], axis = 1, inplace=True)
display(hh[['Пол', 'Возраст']])
| Пол | Возраст | |
|---|---|---|
| 0 | М | 39 |
| 1 | М | 60 |
| 2 | Ж | 36 |
| 3 | М | 38 |
| 4 | Ж | 26 |
| ... | ... | ... |
| 44739 | М | 30 |
| 44740 | М | 27 |
| 44741 | Ж | 48 |
| 44742 | М | 24 |
| 44743 | М | 38 |
44744 rows × 2 columns
- Следующим этапом преобразуем признак "Опыт работы". Его текущий формат - это: <Опыт работы: n лет m месяцев, периоды работы в различных компаниях…>.
Из столбца нам необходимо выделить общий опыт работы соискателя в месяцах, новый признак назовем "Опыт работы (месяц)"
Для начала обсудим условия решения задачи:
- Во-первых, в данном признаке есть пропуски. Условимся, что если мы встречаем пропуск, оставляем его как есть (функция-преобразование возвращает NaN)
- Во-вторых, в данном признаке есть скрытые пропуски. Для некоторых соискателей в столбце стоит значения "Не указано". Их тоже обозначим как NaN (функция-преобразование возвращает NaN)
- В-третьих, нас не интересует информация, которая описывается после указания опыта работы (периоды работы в различных компаниях)
- В-четвертых, у нас есть проблема: опыт работы может быть представлен только в годах или только месяцах. Например, можно встретить следующие варианты:
- Опыт работы 3 года 2 месяца…
- Опыт работы 4 года…
- Опыт работы 11 месяцев…
- Учитывайте эту особенность в вашем коде
Учитывайте эту особенность в вашем коде
В результате преобразования у вас должен получиться столбец, содержащий информацию о том, сколько месяцев проработал соискатель. Выполните преобразование, ответьте на контрольные вопросы и удалите столбец "Опыт работы" из таблицы.
#ваш код здесь
def get_experience(arg):
if arg is np.nan or arg == 'Не указано':
return None
year_words=['год', 'года', 'лет']
month_words=['месяц', 'месяца', 'месяцев']
arg_splitted = arg.split(' ')[:7]
years = 0
months = 0
for index, item in enumerate (arg_splitted):
if item in year_words:
years = int(arg_splitted[index-1])
if item in month_words:
months = int(arg_splitted[index-1])
return int(years*12 + months)
hh['Опыт работы (месяц)'] = hh['Опыт работы'].apply(get_experience)
display(hh['Опыт работы (месяц)'])
0 202.0
1 233.0
2 123.0
3 225.0
4 67.0
...
44739 91.0
44740 84.0
44741 257.0
44742 46.0
44743 190.0
Name: Опыт работы (месяц), Length: 44744, dtype: float64
- Хорошо идем! Следующий на очереди признак "Город, переезд, командировки". Информация в нем представлена в следующем виде: <Город , (метро) , готовность к переезду (города для переезда) , готовность к командировкам>. В скобках указаны необязательные параметры строки. Например, можно встретить следующие варианты:
- Москва , не готов к переезду , готов к командировкам
- Москва , м. Беломорская , не готов к переезду, не готов к командировкам
- Воронеж , готов к переезду (Сочи, Москва, Санкт-Петербург) , готов к командировкам
Создадим отдельные признаки "Город", "Готовность к переезду", "Готовность к командировкам". При этом важно учесть:
Признак "Город" должен содержать только 4 категории: "Москва", "Санкт-Петербург" и "город-миллионник" (их список ниже), остальные обозначьте как "другие".
Список городов-миллионников:
Инфорация о метро, рядом с которым проживает соискатель нас не интересует.million_cities = ['Новосибирск', 'Екатеринбург','Нижний Новгород','Казань', 'Челябинск','Омск', 'Самара', 'Ростов-на-Дону', 'Уфа', 'Красноярск', 'Пермь', 'Воронеж','Волгоград']Признак "Готовность к переезду" должен иметь два возможных варианта: True или False. Обратите внимание, что возможны несколько вариантов описания готовности к переезду в признаке "Город, переезд, командировки". Например:
- … , готов к переезду , …
- … , не готова к переезду , …
- … , готова к переезду (Москва, Санкт-Петербург, Ростов-на-Дону)
- … , хочу переехать (США) , …
Нас интересует только сам факт возможности или желания переезда.
Признак "Готовность к командировкам" должен иметь два возможных варианта: True или False. Обратите внимание, что возможны несколько вариантов описания готовности к командировкам в признаке "Город, переезд, командировки". Например:
- … , готов к командировкам , …
- … , готова к редким командировкам , …
- … , не готов к командировкам , …
Нас интересует только сам факт готовности к командировке.
Еще один важный факт: при выгрузки данных у некоторых соискателей "потерялась" информация о готовности к командировкам. Давайте по умолчанию будем считать, что такие соискатели не готовы к командировкам.
Выполните преобразования и удалите столбец "Город, переезд, командировки" из таблицы.
Совет: обратите внимание на то, что структура текста может меняться в зависимости от указания ближайшего метро. Учите это, если будете использовать порядок слов в своей программе.
#ваш код здесь
def get_city(arg):
# Города-миллионники
million_cities = ['Новосибирск', 'Екатеринбург', 'Нижний Новгород', 'Казань', 'Челябинск', 'Омск',
'Самара', 'Ростов-на-Дону', 'Уфа', 'Красноярск', 'Пермь', 'Воронеж', 'Волгоград']
city = arg.split(' , ')[0]
if (city == 'Москва') or (city == 'Санкт-Петербург'):
return city
elif city in million_cities:
return 'город миллионник'
else:
return 'другие'
def get_ready_to_move(arg):
if ('не готов к переезду' in arg) or ('не готова к переезду' in arg):
return False
elif 'хочу' in arg:
return True
else:
return True
def get_ready_for_bisiness_trips(arg):
if ('командировка' in arg):
if ('не готов к командировкам' in arg) or('не готова к командировкам' in arg):
return False
else:
return True
else:
return False
hh['Город'] = hh['Город, переезд, командировки'].apply(get_city)
hh['Готовность к переезду'] = hh['Город, переезд, командировки'].apply(get_ready_to_move)
hh['Готовность к командировкам'] = hh['Город, переезд, командировки'].apply(get_ready_for_bisiness_trips)
hh = hh.drop('Город, переезд, командировки', axis=1)
display(hh[['Город', 'Готовность к переезду', 'Готовность к командировкам']])
| Город | Готовность к переезду | Готовность к командировкам | |
|---|---|---|---|
| 0 | другие | False | False |
| 1 | другие | False | True |
| 2 | другие | False | False |
| 3 | другие | False | True |
| 4 | Москва | False | True |
| ... | ... | ... | ... |
| 44739 | другие | True | True |
| 44740 | другие | True | True |
| 44741 | город миллионник | True | True |
| 44742 | другие | False | False |
| 44743 | Москва | False | False |
44744 rows × 3 columns
- Рассмотрим поближе признаки "Занятость" и "График". Сейчас признаки представляют собой набор категорий желаемой занятости (полная занятость, частичная занятость, проектная работа, волонтерство, стажировка) и желаемого графика работы (полный день, сменный график, гибкий график, удаленная работа, вахтовый метод).
На сайте hh.ru соискатель может указывать различные комбинации данных категорий, например:
- полная занятость, частичная занятость
- частичная занятость, проектная работа, волонтерство
- полный день, удаленная работа
- вахтовый метод, гибкий график, удаленная работа, полная занятость
Такой вариант признаков имеет множество различных комбинаций, а значит множество уникальных значений, что мешает анализу. Нужно это исправить!
Давайте создадим признаки-мигалки для каждой категории: если категория присутствует в списке желаемых соискателем, то в столбце на месте строки рассматриваемого соискателя ставится True, иначе - False.
Такой метод преобразования категориальных признаков называется One Hot Encoding и его схема представлена на рисунке ниже:
Выполните данное преобразование для признаков "Занятость" и "График", ответьте на контрольные вопросы, после чего удалите их из таблицы
#ваш код здесь
employments = ['полная занятость', 'частичная занятость',
'проектная работа', 'волонтерство', 'стажировка']
charts = ['полный день', 'сменный график',
'гибкий график', 'удаленная работа',
'вахтовый метод']
for employment, chart in zip(employments, charts):
hh[employment] = hh['Занятость'].apply(lambda x: employment in x)
hh[chart] = hh['График'].apply(lambda x: chart in x)
hh = hh.drop('Занятость', axis=1)
hh = hh.drop('График', axis=1)
- (2 балла) Наконец, мы добрались до самого главного и самого важного - признака заработной платы "ЗП".
В чем наша беда? В том, что помимо желаемой заработной платы соискатель указывает валюту, в которой он бы хотел ее получать, например:
- 30000 руб.
- 50000 грн.
- 550 USD
Нам бы хотелось видеть заработную плату в единой валюте, например, в рублях. Возникает вопрос, а где взять курс валют по отношению к рублю?
На самом деле язык Python имеет в арсенале огромное количество возможностей получения данной информации, от обращения к API Центробанка, до использования специальных библиотек, например pycbrf. Однако, это не тема нашего проекта.
Поэтому мы пойдем в лоб: обратимся к специальным интернет-ресурсам для получения данных о курсе в виде текстовых файлов. Например, MDF.RU, данный ресурс позволяет удобно экспортировать данные о курсах различных валют и акций за указанные периоды в виде csv файлов. Мы уже сделали выгрузку курсов валют, которые встречаются в наших данных за период с 29.12.2017 по 05.12.2019. Скачать ее вы можете на платформе
Создайте новый DataFrame из полученного файла. В полученной таблице нас будут интересовать столбцы:
- "currency" - наименование валюты в ISO кодировке,
- "date" - дата,
- "proportion" - пропорция,
- "close" - цена закрытия (последний зафиксированный курс валюты на указанный день).
Перед вами таблица соответствия наименований иностранных валют в наших данных и их общепринятых сокращений, которые представлены в нашем файле с курсами валют. Пропорция - это число, за сколько единиц валюты указан курс в таблице с курсами. Например, для казахстанского тенге курс на 20.08.2019 составляет 17.197 руб. за 100 тенге, тогда итоговый курс равен - 17.197 / 100 = 0.17197 руб за 1 тенге. Воспользуйтесь этой информацией в ваших преобразованиях.
Осталось только понять, откуда брать дату, по которой определяется курс? А вот же она - в признаке "Обновление резюме", в нем содержится дата и время, когда соискатель выложил текущий вариант своего резюме. Нас интересует только дата, по ней бы и будем сопоставлять курсы валют.
Теперь у нас есть вся необходимая информация для того, чтобы создать признак "ЗП (руб)" - заработная плата в рублях.
После ответа на контрольные вопросы удалите исходный столбец заработной платы "ЗП" и все промежуточные столбцы, если вы их создавали.
Итак, давайте обсудим возможный алгоритм преобразования:
- Перевести признак "Обновление резюме" из таблицы с резюме в формат datetime и достать из него дату. В тот же формат привести признак "date" из таблицы с валютами.
- Выделить из столбца "ЗП" сумму желаемой заработной платы и наименование валюты, в которой она исчисляется. Наименование валюты перевести в стандарт ISO согласно с таблицей выше.
- Присоединить к таблице с резюме таблицу с курсами по столбцам с датой и названием валюты (подумайте, какой тип объединения надо выбрать, чтобы в таблице с резюме сохранились данные о заработной плате, изначально представленной в рублях). Значение close для рубля заполнить единицей 1 (курс рубля самого к себе)
- Умножить сумму желаемой заработной платы на присоединенный курс валюты (close) и разделить на пропорцию (обратите внимание на пропуски после объединения в этих столбцах), результат занести в новый столбец "ЗП (руб)".
#ваш код здесь
cur = pd.read_csv("D:/IDE/SF - Assign/ExchangeRates.csv", sep= ",")
def get_salary_num(arg):
salary = float(arg.split(' ')[0])
return salary
def get_salary_currency(arg):
currency_dict = {
'USD': 'USD', 'KZT': 'KZT',
'грн': 'UAH', 'белруб': 'BYN',
'EUR': 'EUR', 'KGS': 'KGS',
'сум': 'UZS', 'AZN': 'AZN'
}
curr = arg.split(' ')[1].replace('.', '')
if curr == 'руб':
return 'RUB'
else:
return currency_dict[curr]
cur['date'] = pd.to_datetime(cur['date'], dayfirst=True).dt.date
hh['Обновление резюме'] = pd.to_datetime(hh['Обновление резюме'], dayfirst=True).dt.date
hh['ЗП (tmp)'] = hh['ЗП'].apply(get_salary_num)
hh['Курс (tmp)'] = hh['ЗП'].apply(get_salary_currency)
merged = hh.merge(
cur,
left_on=['Курс (tmp)', 'Обновление резюме'],
right_on=['currency', 'date',],
how='left'
)
merged['close'] = merged['close'].fillna(1)
merged['proportion'] = merged['proportion'].fillna(1)
hh['ЗП (руб)'] = merged['close'] * merged['ЗП (tmp)'] / merged['proportion']
hh = hh.drop(['ЗП', 'ЗП (tmp)', 'Курс (tmp)'], axis=1)
C:\Users\user\AppData\Local\Temp\ipykernel_3692\1009455752.py:18: UserWarning: Could not infer format, so each element will be parsed individually, falling back to `dateutil`. To ensure parsing is consistent and as-expected, please specify a format. cur['date'] = pd.to_datetime(cur['date'], dayfirst=True).dt.date
Исследование зависимостей в данных¶
- Постройте распределение признака "Возраст". Опишите распределение, отвечая на следующие вопросы: чему равна мода распределения, каковы предельные значения признака, в каком примерном интервале находится возраст большинства соискателей? Есть ли аномалии для признака возраста, какие значения вы бы причислили к их числу?
Совет: постройте гистограмму и коробчатую диаграмму рядом.
# ваш код здесь
fig = px.histogram(hh, x='Возраст', title='Распределение возраста соискателей', marginal='box')
fig.update_layout(xaxis_title='Возраст', yaxis_title='Частота')
fig.show()
Ваши выводы по графику здесь¶
Распределение графика похоже на логнормальное с ассиметрией и не очень выраженным эксцессом. Самый низкий возраст - 14, самый большой - 77 лет. Матожидание находится на уровне 30 лет. Как видно из графика боксплота, медиана находится на уровне 31 года. Точки справа от коробки говорят о наличии выбросов, а именно - с 50 лет. Распределены выбросы линейно до 73 лет, а затем через какойто промежуток идут 76 и 77 лет. Самый дальний выброс наблюдается на уровне 100 лет. Это значение может быть связано как с ненамеренной ошибкой, так и со сделанной специально.
- Постройте распределение признака "Опыт работы (месяц)". Опишите данное распределение, отвечая на следующие вопросы: чему равна мода распределения, каковы предельные значения признака, в каком примерном интервале находится опыт работы большинства соискателей? Есть ли аномалии для признака опыта работы, какие значения вы бы причислили к их числу?
Совет: постройте гистограмму и коробчатую диаграмму рядом.
# ваш код здесь
fig = px.histogram(hh, x='Опыт работы (месяц)', title='Распределение опыта работы', marginal='box')
fig.update_layout(xaxis_title='Опыт работы', yaxis_title='Частота')
fig.show()
Ваши выводы здесь¶
Распределение графика похоже на логнормальное с ассиметрией и не очень выраженным эксцессом. Мода распределения располагается в районе 80-84 месяцев. Самое левое значение на графике распределения соответсвтует 0-4 месяцам, а самое крайнее правое - около 1200 месяцам или 100 годам. Основные наблюдения попадают же в диапазон от 0-4 месяцев до приблизительно 280-284 месяцев (отбросим хвосты). Как видно из графика боксплота, в данных присутствует немалое количество аномалий. Они идут непрерывно приблизительно до показателя в 510 месяцев, а затем становятся более отдаленными друг от друга. Самый крайний выброс находится на уровне 1188 месяцев. Это может быть связано с намеренной или ненамеренной ошибкой или со тех. сбоем
- Постройте распределение признака "ЗП (руб)". Опишите данное распределение, отвечая на следующие вопросы: каковы предельные значения признака, в каком примерном интервале находится заработная плата большинства соискателей? Есть ли аномалии для признака возраста? Обратите внимание на гигантские размеры желаемой заработной платы.
Совет: постройте гистограмму и коробчатую диаграмму рядом.
# ваш код здесь
fig = px.histogram(hh, x='ЗП (руб)', title='Распределение зарплаты', marginal='box')
fig.update_layout(xaxis_title='Зарплата', yaxis_title='Частота')
fig.show()
Ваши выводы здесь¶
Распределение графика похоже на логнормальное с ассиметрией и выраженным эксцессом. Самые крайние левые значения находятся на уровне - (-2500)- 2490 рублей (следует обратить внимание на отрицательные значения, т.к. это, скорее всего ошибка), самое правое - в районе 25 миллионов рублей. Большинство значений расположены в диапозоне от (-2500)- 2490 рублей до 197.5 - 202.49 тыс. рублей (не беря во внимание правый хвост). Аномалии присутствуют, как можно заметить на графике боксплота. Сначала они идут непрерывно и становятся после 1 миллиона все реже. Самое крайнее - 24.3 млн. рублей. Это может быть связано также с ошибками или с очень хорошим мнением о своей уникальности.
- Постройте диаграмму, которая показывает зависимость медианной желаемой заработной платы ("ЗП (руб)") от уровня образования ("Образование"). Используйте для диаграммы данные о резюме, где желаемая заработная плата меньше 1 млн рублей.
Сделайте выводы по представленной диаграмме: для каких уровней образования наблюдаются наибольшие и наименьшие уровни желаемой заработной платы? Как вы считаете, важен ли признак уровня образования при прогнозировании заработной платы?
# ваш код здесь
bar_data = hh[hh['ЗП (руб)']<1e6].groupby('Образование', as_index=False).agg({'ЗП (руб)': 'median'})
fig = px.bar(
data_frame=bar_data,
x='Образование',
y='ЗП (руб)',
title='Медианная з/п по уровню образования'
)
fig.show()
Ваши выводы здесь¶
На графике барплотов мы видим, что на наибольшую зарплату претендуют потенциальные сотрудники с высшим образованием. На наименьшую (около 40 тыс. рублей) претендуют потенциальные сотрудники со средним и средним специальным уровнем образования. Безусловно, эти данные смогут помочь для прогнозирования, например, в моделях классификации или кластеризации и других.
- Постройте диаграмму, которая показывает распределение желаемой заработной платы ("ЗП (руб)") в зависимости от города ("Город"). Используйте для диаграммы данные о резюме, где желая заработная плата меньше 1 млн рублей.
Сделайте выводы по полученной диаграмме: как соотносятся медианные уровни желаемой заработной платы и их размах в городах? Как вы считаете, важен ли признак города при прогнозировании заработной платы?
# ваш код здесь
box_data = hh[hh['ЗП (руб)']<1e6]
fig = px.box(
data_frame=box_data,
x='Город',
y='ЗП (руб)',
color='Город',
title='Распределение з/п по городам'
)
fig.show()
Ваши выводы здесь¶
На графике боксплотов мы видим, что медианная желаемая зарплата зависит от города: сначала - Москва, затем - Санкт-Петербург и так далее. Самый большой размах наблюдается в Москве, при этом и нижнее экстремальное значение находится выше, чем в любом другом городе. В Санкт-Петербурге аналогичная ситуация, что и с медианой, но это менее выражено. Данный признак также может являться болезным при прогнозировании зарплат, ведь, как мы видим, желаемые зарплаты имеют некую тенденцию от города к городу.
- Постройте многоуровневую столбчатую диаграмму, которая показывает зависимость медианной заработной платы ("ЗП (руб)") от признаков "Готовность к переезду" и "Готовность к командировкам". Проанализируйте график, сравнив уровень заработной платы в категориях.
# ваш код здесь
bar_data = hh.groupby(
['Готовность к командировкам', 'Готовность к переезду'],
as_index=False
)['ЗП (руб)'].median()
fig = px.bar(
data_frame=bar_data,
y='Готовность к переезду',
x='ЗП (руб)',
barmode="group",
color='Готовность к командировкам',
orientation='h',
title='Медианная з/п по готовности к командировкам/переезду'
)
fig.show()
Ваши выводы здесь¶
На графике барплотов мы видим некую тенденцию: сосикатели, готовые к командировкам и к переезду расчитывают на наибольшую заработную плату (около 66 тыс. руб.). Следущими идут соискатели, согласные на командировки, но не согласные с переездом (около 60 тыс. руб.). Предпоследними по уровню зарплаты идут согласные к переезду, но не согласные с командировками (50 тыс. рублей). И самый низкий по уровню зарплаты медианный сосискатель, не согласный со всеми этими опциями, претендует на зарплату на уровне 40 тыс. рублей.
- Постройте сводную таблицу, иллюстрирующую зависимость медианной желаемой заработной платы от возраста ("Возраст") и образования ("Образование"). На полученной сводной таблице постройте тепловую карту. Проанализируйте тепловую карту, сравнив показатели внутри групп.
# ваш код здесь
pivot = hh.pivot_table(
index='Образование',
columns='Возраст',
values='ЗП (руб)',
aggfunc='median',
fill_value=0
)
fig = px.imshow(
pivot,
aspect='auto',
color_continuous_scale='greens',
title='Медианная з/п по образованию и возрасту'
)
fig.show()
Ваши выводы здесь¶
Из тепловой карты заметно, что соискатели, претендующие на самую высокую зарплату, 120 тыс. рублей, имеют следующие характеристики: 1.Высшее образование и возраст 16 лет (высшее образование в 16 лет?); 2.Среднее специальное и возраст 67 лет. В целом можно заметить тендцению, что, чем выше уровень образования - тем на больщую зарплату расчитывает соискатель.
- Постройте диаграмму рассеяния, показывающую зависимость опыта работы ("Опыт работы (месяц)") от возраста ("Возраст"). Опыт работы переведите из месяцев в года, чтобы признаки были в едином масштабе. Постройте на графике дополнительно прямую, проходящую через точки (0, 0) и (100, 100). Данная прямая соответствует значениям, когда опыт работы равен возрасту человека. Точки, лежащие на этой прямой и выше нее - аномалии в наших данных (опыт работы больше либо равен возрасту соискателя)
# ваш код здесь
hh['Опыт работы (годы)'] = hh['Опыт работы (месяц)'] / 12
fig = px.scatter(hh, x='Возраст', y='Опыт работы (годы)',
title="Зависимость опыта работы от возраста",
labels={'Опыт работы (годы)': 'Опыт работы (годы)', 'Возраст': 'Возраст'})
fig.add_shape(
type="line",
x0=0, y0=0,
x1=100, y1=100,
line=dict(color="Red", width=2, dash="dash")
)
fig.update_xaxes(range=[0, 100], title="Возраст")
fig.update_yaxes(range=[0, 100], title="Опыт работы (годы)")
fig.show()
Ваши выводы здесь¶
График можно рассмотреть самостоятельно, но стоит обратить внимание на точки, лежащие на либо выше прямой - это аномалии. Точки, лежащие ниже прямой имеют тенденцию к сдвигу вверх, тут все предельно ясно: чем выше возраст - тем выше стаж у сосикателя.
Дополнительные баллы
Для получения 2 дополнительных баллов по разведывательному анализу постройте еще два любых содержательных графика или диаграммы, которые помогут проиллюстрировать влияние признаков/взаимосвязь между признаками/распределения признаков. Приведите выводы по ним. Желательно, чтобы в анализе участвовали признаки, которые мы создавали ранее в разделе "Преобразование данных".
# ваш код здесь
median_salary = hh.groupby(['Пол', 'Образование', 'Город'], as_index=False)['ЗП (руб)'].median()
median_salary.rename(columns={'ЗП (руб)': 'Медианная ЗП'}, inplace=True)
fig = px.sunburst(median_salary, path=['Пол', 'Образование', 'Город'], values='Медианная ЗП',
title='Круговая диаграмма по полу, образованию и городу (медианная ЗП)',
width=800, height=800)
fig.show()
corr = hh[['ЗП (руб)', 'Возраст', 'Опыт работы (месяц)']].corr()
plt.figure(figsize=(10, 8))
sns.heatmap(corr, annot=True, fmt=".2f", cmap='coolwarm', square=True)
plt.title('Коррелограмма')
Text(0.5, 1.0, 'Коррелограмма')
Ваши выводы здесь¶
На круговой диаграмме можно увидеть разбивку по полу, уровню образования и горду проживания по медианной зарплате. Это может быть полезно для наглядного представления этих существующих характеристик у сосикателей на сайте; на коррелограмме можно увидеть тесноту связи таких характеристик зарплата, возраст и опыт работы (при конкретных целях можно добавить другие). Наиболее тесная связь наблюдается у категорий 'Возраст' и 'Опыт работы'. Эта связь довольно понятно и не нуждается в интерпретации.
Очистка данных¶
- Начнем с дубликатов в наших данных. Найдите полные дубликаты в таблице с резюме и удалите их.
# ваш код здесь
dupl = hh[hh.duplicated(subset=hh.columns)]
data = hh.drop_duplicates()
display(dupl.shape[0])
155
- Займемся пропусками. Выведите информацию о числе пропусков в столбцах.
# ваш код здесь
null_data = hh.isnull().sum()
display(null_data[null_data > 0])
Опыт работы 168 Последнее/нынешнее место работы 1 Последняя/нынешняя должность 2 Опыт работы (месяц) 170 Опыт работы (годы) 170 dtype: int64
- Итак, у нас есть пропуски в 3ех столбцах: "Опыт работы (месяц)", "Последнее/нынешнее место работы", "Последняя/нынешняя должность". Поступим следующим образом: удалите строки, где есть пропуск в столбцах с местом работы и должностью. Пропуски в столбце с опытом работы заполните медианным значением.
# ваш код здесь
hh = hh.dropna(subset=['Последнее/нынешнее место работы', 'Последняя/нынешняя должность'])
hh['Опыт работы (месяц)'] = hh['Опыт работы (месяц)'].fillna(hh['Опыт работы (месяц)'].median())
- Мы добрались до ликвидации выбросов. Сначала очистим данные вручную. Удалите резюме, в которых указана заработная плата либо выше 1 млн. рублей, либо ниже 1 тыс. рублей.
# ваш код здесь
outliers = hh[(hh['ЗП (руб)'] > 1e6) | (hh['ЗП (руб)'] < 1e3)]
hh = hh.drop(outliers.index)
- В процессе разведывательного анализа мы обнаружили резюме, в которых опыт работы в годах превышал возраст соискателя. Найдите такие резюме и удалите их из данных
# ваш код здесь
outliers = hh[hh['Опыт работы (месяц)']/12 >= hh['Возраст']]
hh = hh.drop(outliers.index)
- В результате анализа мы обнаружили потенциальные выбросы в признаке "Возраст". Это оказались резюме людей чересчур преклонного возраста для поиска работы. Попробуйте построить распределение признака в логарифмическом масштабе. Добавьте к графику линии, отображающие среднее и границы интервала метода трех сигм. Напомним, сделать это можно с помощью метода axvline. Например, для построение линии среднего будет иметь вид:
histplot.axvline(log_age.mean(), color='k', lw=2)
В какую сторону асимметрично логарифмическое распределение? Напишите об этом в комментарии к графику. Найдите выбросы с помощью метода z-отклонения и удалите их из данных, используйте логарифмический масштаб. Давайте сделаем послабление на 1 сигму (возьмите 4 сигмы) в правую сторону.
Выведите таблицу с полученными выбросами и оцените, с каким возрастом соискатели попадают под категорию выбросов?
# ваш код здесь
fig, ax = plt.subplots(1, 1, figsize=(8, 4))
log_age = np.log(hh['Возраст'] + 1)
histplot = sns.histplot(log_age, bins=30, ax=ax)
histplot.axvline(log_age.mean(), color='k', lw=2)
histplot.axvline(log_age.mean()+ 4 *log_age.std(), color='k', ls='--', lw=2)
histplot.axvline(log_age.mean()- 3 *log_age.std(), color='k', ls='--', lw=2)
histplot.set_title('Log Age Distribution');
def outliers_z_score_mod(hh, feature, left=3, right=3, log_scale=False):
if log_scale:
x = np.log(hh[feature]+1)
else:
x = hh[feature]
mu = x.mean()
sigma = x.std()
lower_bound = mu - left * sigma
upper_bound = mu + right * sigma
outliers = hh[(x < lower_bound) | (x > upper_bound)]
cleaned = hh[(x >= lower_bound) & (x <= upper_bound)]
return outliers, cleaned
outliers, cleaned_data = outliers_z_score_mod(hh, 'Возраст', left=3, right=4, log_scale=True)
display(outliers)
display(list(outliers["Возраст"]))
| Ищет работу на должность: | Опыт работы | Последнее/нынешнее место работы | Последняя/нынешняя должность | Обновление резюме | Авто | Образование | Пол | Возраст | Опыт работы (месяц) | ... | частичная занятость | сменный график | проектная работа | гибкий график | волонтерство | удаленная работа | стажировка | вахтовый метод | ЗП (руб) | Опыт работы (годы) | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 31137 | Менеджер по работе с клиентами | Опыт работы 2 месяца Июнь 2018 — Июль 2018 2... | ООО "ФёрстКэшКомпани" | Менеджер по работе с клиентами | 2019-04-06 | Не указано | среднее | М | 15 | 2.0 | ... | True | True | False | True | False | True | False | False | 10000.0 | 0.166667 |
| 32950 | Тестировщик игр | Опыт работы 3 месяца Март 2019 — по настоящее... | OOO ЖМЫХ | Тестировщик ПО | 2019-04-09 | Не указано | среднее специальное | М | 15 | 3.0 | ... | False | False | False | False | False | False | False | False | 2000.0 | 0.250000 |
| 33654 | Frontend-разработчик | Опыт работы 2 года 6 месяцев Февраль 2019 — п... | Freelance | Frontend-разработчик | 2019-04-19 | Не указано | среднее специальное | М | 100 | 30.0 | ... | True | False | True | True | False | True | True | False | 60000.0 | 2.500000 |
3 rows × 25 columns
[15, 15, 100]
Ваш коммментарий здесь¶
Ассиметрия (скошенность) на графике распределения наблюдается в левую сторону, т.к. правая, вполне, напоминает нормальное. Как можно увидеть в списке возрастов из таблицы выбросов, то под выбросы попадают возраст 15 и 100 лет.